package com.expression.lambda;

import java.io.PrintStream;
import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author abbys
 * jdk8新特性lambda表达式的学习
 * lambda是一个匿名函数，可以吧Lambda表达式理解为一段可以传递的代码
 * Lambda表达式需要函数式接口的支持
 * 函数式接口: 接口中只有一个抽象方法的接口就叫函数式接口  @FunctionalInterface 这个注解可以检查是否是函数式接口
 *
 * 一、 Lambda表达式的基础语法: java8中引入了一个新的操作符 “->” 该操作符称为箭头操作符或者Lambda操作符
 *      箭头操作符将lambda表达式拆分成两部分:
 *      (**)左侧: lambda表达式要用到的参数列表
 *      (**)右侧: lambda表达式中所需执行的功能,即lambda体
 *
 *      抽象方法有几种变体所以语法格式就有这几中不同的格式
 *          函数式接口的抽象方法有 无参无返回值的 有参无返回值的 等等.. 我们下面具体列出
 *      语法格式一: 无参数、无返回值   () -> System.out.println("hello lambda");
 *      语法格式二: 一个参数.无返回值  (x) -> System.out.println(x);  x是参数名称 可以随便取
 *                另外，因为只有一个参数所以小括号可以不写 所以上面也可以写成  x -> System.out.println(x)  但习惯性都是带上小括号的
 *      语法格式三: 两个以上的参数，有返回值 并且 lambda体中有多条语句  (x,y) -> {  具体实现   } 注意：lambda体有多条用{}包裹
 *                两个以上的参数，有返回值 并且 lambda体中只有一条语句 大括号跟return 都可以不写 (x,y) -> 具体实现
 *
 *      语法格式四: Lambda表达式的参数列表的数据类型可以省略不写,因为JVM编译器能通过上下文推断出数据类型，即“类型推断”  因为接口中定义了
 *                要自己写数据类型也可以 (Integer x,Integer y) -> 具体实现
 *
 * 二、 Java8内置的四大核心函数式接口  (会用就行)
 *      Consumer<T> : 消费型接口
 *          void accept(T t);
 *      suppiler<T> : 供给型接口
 *          T get();
 *      Function<T,R> : 函数型接口 T代表参数类型 R代表返回类型
 *          R apply(T t);
 *      Predicate<T> : 断言型接口
 *          boolean test(T t);
 *
 *       我们对jdk8提供的内置函数式接口用Lambda表达式传递代码块实现其实也就是生成了他们的匿名内部类(实现),所以就可以直接用了
 *
 * 三、方法引用
 *     若Lamda体中的内容有方法已经实现了,我们可以使用方法引用(可以理解为方法引用是Lambda表达式的另外一种表现形式)
 *     主要有三种语法格式:
 *          对象::实例方法名
 *          类::静态方法名
 *          类::实例方法名
 *
 * 四、 构造器引用
 *      类名::new
 *
 * 五、 数组引用
 *      Type[]::new
 *
 *  ------------------------------------------------------------------------------------------------
 *
 *  Stream Api
 *     Stream的三个操作步骤:
 *     创建Stream  有stream对象才能进行中间操作
 *          1(**).通过集合接口Collection的 stream()方法得到串行流 或者 通过parallelStream()方法得到并行流
 *          2.Arrays的静态方法 stream() 获得数组流
 *          3.通过Stream类中的静态方法 of()
 *          4.通过Stream的iterate()或者generate()生成无限流 无限流就是会一直生成产生数据的流
 *
 *     中间操作    对源数据的一些操作 生成新流
 *          1.filter    接收Lambda,从流中过滤某些元素
 *          2.limit     限制需要的个数 只取前n个
 *          3.skip(n)   扔掉前n个元素 如果源数据不足n个就返回空流
 *          4.distinct  去除重复元素   是通过hashcode跟equal方法来判断是否是重复元素的
 *          5.map       接收Lambda 接收函数作为参数 该参数会被作用到流中的每个元素上，并将其映射成新的元素
 *          6.sorted    排序 自然排序/比较器排序A
 *     终止操作   对新流做什么
 *          查找与匹配
 *              allMatch    是否匹配所有元素
 *              anyMatch    是否至少匹配一个元素
 *              noneMatch   是否没有匹配所有元素
 *              findFirst   返回匹配的第一个元素
 *              findAny     返回当前流中匹配的任意元素
 *              count       返回流中元素的总个数
 *              max         返回流中最大值
 *              min         返回流中最小值
 *
 *          归约
 *              reduce(T,BinaryOperator) / reduce(BinaryOprator) -可以将流中元素反复结合起来，得到一个值 比如求流中所有元素的合
 *
 *          收集
 *              collect  用于给stream中的元素做汇总的方法  参数接受一个collector接口的实现 一般常用Collectors工具类来生成
 *              比如元素求平均值啊，元素取出存到另一个容器啊等
 *
 *
 *  Optional
 *     是jdk8提供的一歌较好避免空指针异常的一种容器
 *     常用方法
 *      Optional.of(T t): 创建一个Optional实例,该实例内放着T的实例
 *      Optional.empty(): 创建一个空的Option实例  这个Optional里面没包含值的意思
 *      Optional.ofNullable(T t): 若t不为null,创建Optional实例，否则返回空Optional实例
 *      isPresent() 判断是否包含值
 *      orElse(T t): 如果有包含值就返回没有就返回t
 *
 * 补充jdk8新特性: 接口中有默认方法了 例如 public default String getName(){} 实现这个接口的可以直接使用这个方法 重载的话类优先原则
 *               接口中可以有静态方法  接口名.静态方法名
 *
 */
public class Main {

    public static void main(String[] args) {

        test2();

        test3();

        demo1();

        demo2();

        System.out.println("------------以上都是 一 的案例-----------");
        //内置的四大核心函数式接口 跟 demo1跟demo2 差不多一样使用 会用就行

        test6();

        test7();

        test8();

        test9();
        System.out.println("------------以上都是 三跟四 的案例-----------");

        streamApiTest1();

        System.out.println("--------------以上是Stream案例-------------------");

        testMatch();

        testReduce();

        testCollection();

    }

    /**
     * 以前的匿名内部类跟 lambda 表达式的匿名内部类写法区分
     */
    public static void test1(){

        //jdk8以前匿名内部类的写法
        Comparator<Integer> com =  new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return Integer.compare(o1,o2);
            }
        };
        //然后com在这里当做参数可以直接用了
        TreeSet<Integer> set = new TreeSet<>(com);
        //会发现匿名内部类整个写法其实只有 return Integer.compare(o1,o2);是有用的，其他的都是必须但是没必要的

        System.out.println("------------------------------");

        //lambda的匿名内部类写法
        Comparator<Integer> comparator = (x,y) ->Integer.compare(x,y);

        TreeSet<Integer> treeSet = new TreeSet<>(comparator);

    }


    /**
     * 查询年龄在35岁以上的员工信息
     */
    public static void test2(){

        //Arrays.asList() 的参数是可变参数
        List<Employee> employees = Arrays.asList(
            new Employee("张三",18,9999.99),
            new Employee("李四",38,5555.99),
            new Employee("王五",50,6666.99),
            new Employee("赵六",16,3333.99),
            new Employee("田七",8,7777.99)
        );

        //jdk8以前是这样写的
        List<Employee> emp = new ArrayList<>();
        for (Employee e : employees) {
            if(e.getAge()>=35){
                emp.add(e);
            }
        }
        //然后emp就是需求所得结果
        for(Employee e: emp){
            System.out.println(e);
        }

        System.out.println("---------------------------------");

        /**
         * lambda表达式写法 先把集合流化 然后对这个流进行过滤，流中的每个数据是当做e 对过滤完后的集合进行foreach循环循环所做的事是打印输出
         * filter(): 把集合中的数据按照某种规则过滤后的集合
         */
        employees.stream()
                .filter((e) -> e.getAge() >=35)
                .forEach(System.out::println);


        System.out.println("---------------------------------");

        /**
         * 把集合中的每个Employee名字都列出来,只列前3个
         *  map():    把集合中匹配条件的取出
         *  limit():  限制结果的条数
         *
         */
        employees.stream()
                .map(Employee::getName)
                .limit(3)
                .forEach(System.out::println);

    }

    /**
     * Lambda表达式 语法格式一: 无参数、无返回值 案例
     */
    public static void test3(){

        int num = 0; //jdk8以前 匿名内部类内部要使用同级别的变量时必须加final jdk8以后不需要手动加了默认给我们加了

        //Runnable就是一个函数式接口 它里面只有run()这一个抽象方法且这个抽象方法是没有参数跟没有返回值的
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("jdk8之前的写法"+num); //本质还是final所以这个num还是不可变的只是final不用我们手动写了
            }
        };
        r.run();

        //这里面就算没写是对谁的实现也知道肯定是对run()方法的实现 因为函数式接口
        //这里就用Lambda表达式对这个Runnable的run()方法做了一个实现，具体的实现是System.out.println("JDK8后的Lambda表达式写法")
        Runnable r2 = () -> System.out.println("JDK8后的Lambda表达式写法");
        r2.run();
    }

    /**
     * Lambda表达式 语法格式二: 一个参数、无返回值 案例
     */
    public static void test4(){
        //Consumer 我们可以点进去看看 发现里面有个  void accept(T t); 的抽象方法 就是一个参数无返回值的
        //这里就用Lambda表达式对这个Consumer的accept方法做了一个实现，具体实现是System.out.println(传递的参数)
        Consumer<String> con = (x) -> System.out.println(x);

    }

    /**
     * lambda表达式 语法格式三: 多个参数,有返回值 Lambda体中有多条语句
     */
    public static void test5(){
        //Comparator接口方法内有int compare(T o1, T o2) 抽象方法
        //之前lambda体都是一行代码就搞定了就直接写就行，现在如果具体实现超过一行就必须得用{}包裹起来
        Comparator<Integer> com = (x,y) ->{
            int compare = Integer.compare(x, y);
            return compare;
        };

        //第二中情况 如果Lambda体中只有一条语句 有参有返回值 那么 大括号 跟 return 也是可以省略的  返回值会帮你带回去则我们可以省略不写
        Comparator<Integer> com2 = (x,y) -> Integer.compare(x, y);

    }


    /**
     * 做一个小练习 对一个数进行运算
     * 实质可以有很多种运算 比如求和 相减等 这里是设计模式之策略模式的一种思想体现
     * 自己写一个函数式接口
     *
     * 策略模式: 把要做的策略写成一个接口,以后不同的策略就是不同的具体实现
     * 然后写一个来调用这个策略接口的方法 如下面的 operation 方法
     * 然后我们在使用的时候只需要自己做出策略具体的实现类 然后调用operation方法传入自己的策略就可以按照自己的策略来完成功能了  如本方法
     *
     */
    public static void demo1(){
        //策略模式 根据不同策略 实现 不同运算
        //可能原先我们做的时候 要写两个方法 一个用于算平方 一个用于算求和 现在只要通过传递的实现不同就可以一个方法搞定

        Integer num = operation(100,(x) -> x*x);
        System.out.println(num);     //10000

        Integer num2 = operation(200,(x)->x+x);
        System.out.println(num2); //400
    }

    private static Integer operation(Integer num,MyFuncrion mf){
        return mf.getValue(num);
    }


    /**
     * 小练习 调用Collections.sort()方法，通过定制排序比较两个Employee(先按年龄比，年龄相同按姓名比),使用Lambda作为参数传递
     */

    public static void demo2(){

        List<Employee> employees = Arrays.asList(
                new Employee("张三",18,9999.99),
                new Employee("李四",38,5555.99),
                new Employee("王五",50,6666.99),
                new Employee("赵六",16,3333.99),
                new Employee("田七",8,7777.99)
        );

        Collections.sort(employees,(e1,e2)->{
            if(e1.getAge() == e2.getAge()){
                return e1.getName().compareTo(e2.getName());
            }else{
                return Integer.compare(e1.getAge(),e2.getAge());
            }
        });

        for (Employee e: employees){
            System.out.println(e);
        }

    }

    /**
     * 方法引用案例一 对象::实例方法名
     */
    public static void test6(){
        Employee employee = new Employee();
        employee.setAge(18);
        employee.setName("小明");

        //供给形函数式接口 T get()  用Lambda表达式的箭头操作符来实现
        Supplier<String> sup = () -> employee.getName();
        String s = sup.get();
        System.out.println(s); //小明

        //用方法引用来实现
        Supplier<Integer> sup2 = employee::getAge;  //lambda体已经有实现且实现是employee的getAge方法
        Integer i = sup2.get();
        System.out.println(i);   //18

    }

    /**
     * 方法引用案例二 类::静态方法名
     */
    public static void test7(){
        //用Lambda表达式的箭头操作符来实现
        Comparator<Integer> comparator = (x,y) -> Integer.compare(x,y);
        int compare = comparator.compare(10, 20);
        System.out.println(compare);

        //用方法引用来实现
        Comparator<Integer> comparator1 =Integer::compare;
        int compare1 = comparator1.compare(50, 40);
        System.out.println(compare1);
    }

    /**
     * 方法引用案例三 类::实例方法
     * 比如我们做一个比较两个字符串是否相等   断言型函数式接口 Predicate 有个 boolean test(T t); 但是我们是比较两个 所以我们可以用他的子类
     */
    public static void test8(){

        //用Lambda表达式的箭头操作符来实现
        BiPredicate<String,String> bp = (x,y) -> x.equals(y);

        //用方法引用来实现
        //注意 只有 第一个参数是这个类的这个方法的调用者且第二个参数是这个方法需要的参数时，才能这么用
        BiPredicate<String,String> bp2 = String::equals;
        boolean flag = bp2.test("abc", "cbd");//就是 "abc".equals("cbd")
        System.out.println(flag);

    }

    /**
     * 构造器引用案例
     */
    public static void test9(){
        //用Lambdab表达式的箭头操作符来实现
        Supplier<Employee> supplier1 =() -> new Employee();

        //用构造器引用来实现
        //调用的是无参构造器 因为Employee::new相当于是Supplier的实现这个类的抽象方法是空参的所以调用的构造器也是空参的
        Supplier<Employee> supplier = Employee::new;
        Employee employee = supplier.get();
        System.out.println(employee);

        //如果想要调用有参的构造器怎么办呢？ 可以使用函数型Function<T,R>  T代表参数类型 R代表返回类型  R apply(T t)
        Function<Integer,Employee> function = Employee::new; //调用的就是参数为Integer的构造器
        Employee applyEmployee = function.apply(20);
        System.out.println(applyEmployee);
    }


    /**
     * StreamApi: 就是在数据传输过程中把数据变成流以至于可以做一系列流水线式的中间操作,经过这些操作后会产生一个新流，对原数据没有影响
     * 中间操作比如过滤 匹配 限制个数等
     * 然后通过终止条件得到新的流
     * 终止条件比如打印 添加等
     */
    public static void streamApiTest1(){

        List<Employee> employees = Arrays.asList(
                new Employee("张三",18,9999.99),
                new Employee("李四",38,5555.99),
                new Employee("王五",50,6666.99),
                new Employee("赵六",16,3333.99),
                new Employee("田七",8,7777.99)
        );


        employees.stream()                              //获取流
                .filter( (e)-> e.getAge()>35)           //中间操作
                .forEach(System.out::println);          //终端操作

        employees.stream()
                .map((e)->e.getName())
                .forEach(System.out::println);

    }

    /**
     * 测试终止条件的查找与匹配
     */
    public static void testMatch(){

        List<Employee> employees = Arrays.asList(
                new Employee("张三",18,9999.99),
                new Employee("李四",38,5555.99),
                new Employee("王五",50,6666.99),
                new Employee("赵六",16,3333.99),
                new Employee("田七",8,7777.99)
        );


        //Optional是JDK尽可能避免空指针异常的一个容器，他把返回的数据封装到Optional中然后如果为空可以做一些补救措施
        Optional<Employee> op = employees.stream()
                .sorted((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()))
                .findFirst();

        //op.orElse(other); 如果Optional容器中是空的可以用另一个对象来代替

        System.out.println(op.get());


    }

    /**
     *  测试归约 把数组里的所有值求和
     */
    public static void testReduce(){
        List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);

        //第一个参数 从哪开始，第二个参数是个Function的子接口
        //原理 首先把 0当做x然后从流中取出一个元素当做y然后执行操作，然后把得到的值又当做x,再从流中又拿出个当y,又操作
        Integer sum = list.stream()
                .reduce(0, (x, y) -> x + y);

        System.out.println(sum);

    }

    /**
     * 测试收集 求平均 求和 分组
     */
    public static void testCollection(){

        List<Employee> employees = Arrays.asList(
                new Employee("张三",18,9999.99),
                new Employee("李四",38,5555.99),
                new Employee("王五",50,6666.99),
                new Employee("赵六",16,3333.99),
                new Employee("田七",8,7777.99),
                new Employee("田七",9,7778.99)
        );

        //List
        List<String> nameList = employees.stream()
                .map(Employee::getName)
                .collect(Collectors.toList());

        for (String s : nameList) {
            System.out.println(s);
        }

        //set去重
        Set<String> nameSet = employees.stream()
                .map(Employee::getName)
                .collect(Collectors.toSet());

        nameSet.forEach(System.out::println);


        //特殊一点的数据结构
        Collection<String> nameHashSet = employees.stream()
                .map(Employee::getName)
                .collect(Collectors.toCollection(HashSet::new));

        nameHashSet.forEach(System.out::println);


        //获取工资平均值
        Double avgSalary = employees.stream()
                .collect(Collectors.averagingDouble(Employee::getSalary));

        System.out.println("平均值工资是:"+avgSalary);

        //求工资总和
        Double sumSalary = employees.stream()
                .collect(Collectors.summingDouble(Employee::getSalary));
        System.out.println("工资总和是"+sumSalary);

        //对流分组  对流中的元素把名字分组  每一组有多个数据所以是Map嵌套List
        Map<String, List<Employee>> groupCollect = employees.stream()
                .collect(Collectors.groupingBy(Employee::getName));

        System.out.println(groupCollect);

        System.out.println("----------------------");

        //多级分组 先按照名字分组 在名字组内又按年龄分
        Map<String, Map<Integer, List<Employee>>> nameAgeCollect = employees.stream()
                .collect(Collectors.groupingBy(Employee::getName, Collectors.groupingBy(Employee::getAge)));
        System.out.println(nameAgeCollect);

        //分区  满足条件的一个区不满足条件的一个区     大于8000工资的一个区 不大于的一个区
        Map<Boolean, List<Employee>> booleanMap = employees.stream()
                .collect(Collectors.partitioningBy(e -> e.getSalary() > 8000));

        System.out.println(booleanMap);

        //把所有名字拼接在一起 用,分割
        String joingName = employees.stream()
                .map(Employee::getName)
                .collect(Collectors.joining(","));

        System.out.println(joingName);


    }



}
